/*
 * Copyright (c) 2013-2018 上海汇数数据. All rights reserved.
 * @(#) JacksonJsonOperator.java 2018-08-06 16:47
 */

package cn.jh.common.core.json;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.google.common.collect.MapMaker;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentMap;

import static com.google.common.base.Preconditions.checkArgument;


/**
 * @author Fuchun
 * @since 1.0
 */
public class JacksonJsonOperator extends AbstractJsonOperator {

    private final ConcurrentMap<Class<?>, ObjectWriter> viewWriters =
            new MapMaker().initialCapacity(10).makeMap();

    private ObjectMapper objectMapper = new ObjectMapper();
    private ObjectWriter prettyWriter = objectMapper.writerWithDefaultPrettyPrinter();

    public JacksonJsonOperator() {
        super();
        initializeObjectMapper();
    }

    public JacksonJsonOperator(ObjectMapper objectMapper) {
        super();
        this.objectMapper = objectMapper;
    }

    private void initializeObjectMapper() {
        objectMapper.findAndRegisterModules();
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        objectMapper.setTimeZone(TimeZone.getTimeZone("UTC"));

        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
        objectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true);

        prettyWriter = objectMapper.writerWithDefaultPrettyPrinter();
    }

    @Override
    public String toJSONString(Object object, Class<?> viewClass, boolean prettyPrinter) {
        if (viewClass == null) {
            try {
                if (prettyPrinter) {
                    return prettyWriter.writeValueAsString(object);
                } else {
                    return objectMapper.writeValueAsString(object);
                }
            } catch (JsonProcessingException ex) {
                throw new JsonParseException(ex);
            }
        }
        ObjectWriter writer = viewWriters.computeIfAbsent(viewClass, k -> objectMapper.writerWithView(viewClass));

        try {
            if (prettyPrinter) {
                return writer.withDefaultPrettyPrinter().writeValueAsString(object);
            } else {
                return writer.writeValueAsString(object);
            }
        } catch (JsonProcessingException ex) {
            throw new JsonParseException(ex);
        }
    }

    @Override
    public byte[] toJSONBytes(Object object, Class<?> viewClass) {
        if (viewClass == null) {
            try {
                return objectMapper.writeValueAsBytes(object);
            } catch (JsonProcessingException ex) {
                throw new JsonParseException(ex);
            }
        }
        ObjectWriter writer = viewWriters.computeIfAbsent(viewClass, k -> objectMapper.writerWithView(viewClass));
        try {
            return writer.writeValueAsBytes(object);
        } catch (JsonProcessingException ex) {
            throw new JsonParseException(ex);
        }
    }

    @Override
    public void writeValue(OutputStream output, Object object) throws IOException {
        writeValue(output, object, false);
    }

    @Override
    public void writeValue(OutputStream output, Object object, boolean prettyPrinter) throws IOException {
        checkArgument(output != null, "output must be not null");
        checkArgument(object != null, "object must be not null");
        try {
            if (prettyPrinter) {
                prettyWriter.writeValue(output, object);
            } else {
                objectMapper.writeValue(output, object);
            }
        } catch (JsonGenerationException | JsonMappingException ex) {
            throw new JsonParseException(ex);
        }
    }

    @Override
    @SuppressWarnings("TypeParameterUnusedInFormals")
    public <T> T convert(Map<String, Object> map, Type type) {
        if (map == null) return null;
        try {
            return objectMapper.convertValue(map, toJavaType(type));
        } catch (Exception ex) {
            throw rethrow(map, type, ex);
        }
    }

    @Override
    protected <T> T doConvert(Map<String, Object> map, Class<T> targetClass) {
        try {
            return objectMapper.convertValue(map, targetClass);
        } catch (Exception ex) {
            throw rethrow(map, targetClass, ex);
        }
    }

    @Override
    public <T> T parseObject(String jsonString, Class<T> targetClass) {
        return readValue(jsonString, targetClass);
    }

    @Override
    @SuppressWarnings("TypeParameterUnusedInFormals")
    public <T> T parseObject(String jsonString, Type type) {
        return readValue(jsonString, toJavaType(type));
    }

    @Override
    public <T> T parseObject(File file, Class<T> targetClass) throws IOException {
        checkArgument(file != null && file.exists(), "The file must be not null and exists.");
        checkArgument(file.isFile(), "The file is a directory?");
        try {
            return objectMapper.readValue(file, targetClass);
        } catch (com.fasterxml.jackson.core.JsonParseException | JsonMappingException ex) {
            throw new JsonParseException(ex);
        }
    }

    @Override
    @SuppressWarnings("TypeParameterUnusedInFormals")
    public <T> T parseObject(File file, Type targetType) throws IOException {
        checkArgument(file != null && file.exists(), "The file must be not null and exists.");
        checkArgument(file.isFile(), "The file is a directory?");
        try {
            return objectMapper.readValue(file, toJavaType(targetType));
        } catch (com.fasterxml.jackson.core.JsonParseException | JsonMappingException ex) {
            throw new JsonParseException(ex);
        }
    }

    @Override
    public <T> T parseObject(InputStream input, Class<T> targetClass) throws IOException {
        try {
            return objectMapper.readValue(input, targetClass);
        } catch (com.fasterxml.jackson.core.JsonParseException | JsonMappingException ex) {
            throw new JsonParseException(ex);
        }
    }

    @Override
    @SuppressWarnings("TypeParameterUnusedInFormals")
    public <T> T parseObject(InputStream input, Type targetType) throws IOException {
        try {
            return objectMapper.readValue(input, toJavaType(targetType));
        } catch (com.fasterxml.jackson.core.JsonParseException | JsonMappingException ex) {
            throw new JsonParseException(ex);
        }
    }

    @Override
    protected <C extends Collection<T>, T> C doParseToCollection(
            String jsonString, Class<C> listClass, Type elementType) {
        JavaType colType = TypeFactory.defaultInstance()
                .constructCollectionType(listClass, toJavaType(elementType));
        return readValue(jsonString, colType);
    }

    @Override
    protected <K, V, M extends Map<K, V>> M doParseToMap(
            String jsonString, Class<M> mapClass, Class<K> keyClass, Class<V> valueClass) {
        JavaType mapType = TypeFactory.defaultInstance()
                .constructMapType(mapClass, keyClass, valueClass);
        return readValue(jsonString, mapType);
    }

    @Override
    protected <K, V> Map<K, V> doParseToMap(String jsonString, Type keyType, Type valueType) {
        JavaType mapType = TypeFactory.defaultInstance()
                .constructMapType(Map.class, toJavaType(keyType), toJavaType(valueType));
        return readValue(jsonString, mapType);
    }

    private <T> T readValue(String jsonString, Class<T> tClass) {
        try {
            return objectMapper.readValue(jsonString, tClass);
        } catch (IOException ex) {
            throw new JsonParseException(ex);
        }
    }

    @SuppressWarnings("TypeParameterUnusedInFormals")
    private <T> T readValue(String jsonString, JavaType javaType) {
        try {
            return objectMapper.readValue(jsonString, javaType);
        } catch (IOException ex) {
            throw new JsonParseException(ex);
        }
    }

    private static JavaType toJavaType(Type type) {
        JavaType javaType;
        if (type instanceof JavaType) {
            javaType = (JavaType) type;
        } else {
            // Fixed Generic Type (ParameterizedType)
            TypeFactory typeFactory = TypeFactory.defaultInstance();
            if (type instanceof ParameterizedType) {
                ParameterizedType pType = (ParameterizedType) type;
                Type[] argTypes = pType.getActualTypeArguments();
                Type ownerType = pType.getRawType();
                JavaType[] argJavaTypes = new JavaType[argTypes.length];
                for (int i = 0; i < argTypes.length; i++) {
                    argJavaTypes[i] = typeFactory.constructType(argTypes[i]);
                }
                javaType = typeFactory.constructSimpleType((Class<?>) ownerType, argJavaTypes);
            } else {
                javaType = typeFactory.constructType(type);
            }
        }
        return javaType;
    }

    public ObjectMapper getObjectMapper() {
        return objectMapper;
    }

    public void setObjectMapper(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    public void refreshPrettyWriter() {
        this.prettyWriter = objectMapper.writerWithDefaultPrettyPrinter();
    }
}
